/*
 * Copyright (C) 2008 Peter Kling
 * 
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 * 
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 'A Kind of Billiard'. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package de.akob.misc;

import java.awt.Frame;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.media.opengl.DebugGL;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.glu.GLU;

import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.GLUT;

import de.akob.mathematics.MathVector;
import de.akob.mathematics.Mathematics;
import de.akob.mathematics.Quaternion;

public abstract class JOGLWindowWithNavigation extends Frame implements GLEventListener, MouseListener,
        MouseMotionListener, KeyListener {
	// generate glu and glut objects
	protected GLU        glu               = new GLU();
	protected GLUT       glut              = new GLUT();
	
	// camera position/orientation
	protected MathVector cameraPos         = new MathVector(0., 0., 10.);
	protected Quaternion cameraOrientation = new Quaternion(1., 0., 0., 0.);
	
	// frustum data
	protected double     frustum_nearPlane = 0.1;
	protected double     frustum_farPlane  = 100.;
	protected double     frustum_fovY      = 30.;
	protected double     frustum_aspect    = 1.;
	
	// window/screen data
	protected int        width_window      = 0;
	protected int        height_window     = 0;
	protected double     width_nearPlane   = 0.;
	protected double     height_nearPlane  = 0.;
	protected double     width_farPlane    = 0.;
	protected double     height_farPlane   = 0.;
	
	// we use a frames-per-second animator
	private FPSAnimator  animator;
	
	// mouse coordinates from last mouse event
	protected int        x_mouse_prev, y_mouse_prev;
	
	/**
	 * Standard constructor.
	 */
	public JOGLWindowWithNavigation() {
		// request bits for stencil buffer
		GLCapabilities glCapabilities = new GLCapabilities();
		glCapabilities.setStencilBits(8);
		
		// generate canvas, link it to the OpenGL context and add it to this window
		GLCanvas canvas = new GLCanvas(glCapabilities);
		canvas.addGLEventListener(this);
		add(canvas);
		
		// generate animator
		animator = new FPSAnimator(canvas, 60);
		
		// stop rendering and shut down the application when window is closed
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				animator.stop();
				System.exit(0);
			}
		});
	}
	
	/**
	 * Applies camera translation and rotation to the current OpenGL matrix.
	 */
	protected void applyViewTransformation(GL gl) {
		double angle = Mathematics.rad2deg(cameraOrientation.getAngle());
		MathVector axis = cameraOrientation.getAxis();
		gl.glRotated(-angle, axis.get(1), axis.get(2), axis.get(3));
		gl.glTranslated(-cameraPos.get(1), -cameraPos.get(2), -cameraPos.get(3));
	}
	
	/**
	 * Display method for OpenGL.
	 */
	public abstract void display(GLAutoDrawable drawable);
	
	/**
	 * Support for on-the-fly screen mode switching (not yet implemented by jogl).
	 */
	public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) { /* void */
	}
	
	/**
	 * Initializes the OpenGL context.
	 */
	public void init(GLAutoDrawable drawable) {
		drawable.setGL(new DebugGL(drawable.getGL())); // use the debug pipeline during development
		
		// register some input listeners for user interaction
		drawable.addMouseListener(this);
		drawable.addMouseMotionListener(this);
		drawable.addKeyListener(this);
	}
	
	/**
	 * Keyboard handling.
	 * 
	 * @param e key event
	 */
	public void keyPressed(KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_W) {
			MathVector direction = new MathVector(0., 0., -1.);
			direction.mul(0.1);
			direction = Mathematics.rotation3D(direction, cameraOrientation);
			cameraPos.add(direction);
		}
		
		if (e.getKeyCode() == KeyEvent.VK_A) {
			MathVector direction = new MathVector(-1., 0., 0.);
			direction.mul(0.1);
			direction = Mathematics.rotation3D(direction, cameraOrientation);
			cameraPos.add(direction);
		}
		
		if (e.getKeyCode() == KeyEvent.VK_S) {
			MathVector direction = new MathVector(0., 0., 1.);
			direction.mul(0.1);
			direction = Mathematics.rotation3D(direction, cameraOrientation);
			cameraPos.add(direction);
		}
		
		if (e.getKeyCode() == KeyEvent.VK_D) {
			MathVector direction = new MathVector(1., 0., 0.);
			direction.mul(0.1);
			direction = Mathematics.rotation3D(direction, cameraOrientation);
			cameraPos.add(direction);
		}
	}
	
	/**
	 * Keyboard handling.
	 * 
	 * @param e key event
	 */
	public void keyReleased(KeyEvent e) {
	}
	
	/**
	 * Keyboard handling.
	 * 
	 * @param e key event
	 */
	public void keyTyped(KeyEvent e) {
	}
	
	/**
	 * Mouse handling.
	 * 
	 * @param e mouse event
	 */
	public void mouseClicked(MouseEvent e) {
	}
	
	/**
	 * Mouse handling.
	 * 
	 * @param e mouse event
	 */
	public void mouseDragged(MouseEvent e) {
		// get current mouse coordinates
		int x_mouse = e.getX();
		int y_mouse = e.getY();
		
		// rotation
		if ((e.getModifiers() & InputEvent.BUTTON1_MASK) != 0) {
			double angleX = 2 * Math.PI * (y_mouse - y_mouse_prev) / height_window;
			double angleY = -2 * Math.PI * (x_mouse - x_mouse_prev) / width_window;
			MathVector axisX = Mathematics.getUnitVector(3, 1);
			MathVector axisY = Mathematics.getUnitVector(3, 2);
			axisX = Mathematics.rotation3D(axisX, cameraOrientation);
			axisY = Mathematics.rotation3D(axisY, cameraOrientation);
			Quaternion rotation1 = new Quaternion(angleX, axisX);
			Quaternion rotation2 = new Quaternion(angleY, axisY);
			cameraOrientation = Mathematics.mul(rotation1, cameraOrientation);
			cameraOrientation = Mathematics.mul(rotation2, cameraOrientation);
		}
		
		// translation
		if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0) {
			double transX = -5. * (x_mouse - x_mouse_prev) / width_window;
			double transY = 5. * (y_mouse - y_mouse_prev) / height_window;
			MathVector tx = Mathematics.mul(transX, Mathematics.getUnitVector(3, 1));
			MathVector ty = Mathematics.mul(transY, Mathematics.getUnitVector(3, 2));
			MathVector direction = Mathematics.add(tx, ty);
			direction = Mathematics.rotation3D(direction, cameraOrientation);
			cameraPos.add(direction);
		}
		
		// update previous mouse coordinates for next event
		x_mouse_prev = x_mouse;
		y_mouse_prev = y_mouse;
	}
	
	/**
	 * Mouse handling.
	 * 
	 * @param e mouse event
	 */
	public void mouseEntered(MouseEvent e) {
	}
	
	/**
	 * Mouse handling.
	 * 
	 * @param e mouse event
	 */
	public void mouseExited(MouseEvent e) {
	}
	
	/**
	 * Mouse handling.
	 * 
	 * @param e mouse event
	 */
	public void mouseMoved(MouseEvent e) {
	}
	
	/**
	 * Mouse handling.
	 * 
	 * @param e mouse event
	 */
	public void mousePressed(MouseEvent e) {
		x_mouse_prev = e.getX();
		y_mouse_prev = e.getY();
	}
	
	public void mouseReleased(MouseEvent e) { /* void */
	}
	
	/**
	 * Called on a reshape event.
	 */
	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		GL gl = drawable.getGL();
		
		// set viewport
		gl.glViewport(x, y, width, height);
		
		// set projection
		frustum_aspect = (double) width / (double) height;
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		glu.gluPerspective(frustum_fovY, frustum_aspect, frustum_nearPlane, frustum_farPlane);
		gl.glMatrixMode(GL.GL_MODELVIEW);
		
		// store dimensions
		width_window = width;
		height_window = height;
		height_nearPlane = 2*Math.tan(Mathematics.deg2rad(frustum_fovY/2.)) * frustum_nearPlane;
		width_nearPlane = frustum_aspect*height_nearPlane;
		height_farPlane = 2*Math.tan(Mathematics.deg2rad(frustum_fovY/2.)) * frustum_farPlane;
		width_farPlane = frustum_aspect*height_farPlane;
	}
	
	/**
	 * Whether to hide or show this window
	 * 
	 * @param isVisible true if window is to be shown, false otherwise
	 */
	@Override
	public void setVisible(boolean isVisible) {
		super.setVisible(isVisible);
		if (isVisible) {
			animator.start();
		} else {
			animator.stop();
		}
	}
}
